/*
 ------------------------------------------------------------------------------
 Copyright (C) 2013 Eternal Games.

 This file is part of the EternalTech source code.

 The EternalTech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The EternalTech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the EternalTech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// defManager.cpp - definition manager
//


#include "common.h"


#define DEFS_GROW_SIZE			1024

#define DEFS_HASH_SIZE			1024

struct def_t {
	bool				registered;

	defType_t			type;
	const char *		typeName;

	char				filePath[MAX_PATH_LENGTH];
	char				fileExtension[MAX_PATH_LENGTH >> 4];

	char				listCommand[32];
	char				printCommand[32];

	int					numDefs;
	int					maxDefs;
	defData_t **		defs;

	defData_t *			hashTable[DEFS_HASH_SIZE];
};

static def_t			def_defTypes[DEF_MAX_TYPES];


/*
 ==============================================================================

 SUPPORT ROUTINES

 ==============================================================================
*/


/*
 ==================
 Def_DefTypeForCommand

 Returns a definition type for the current command
 ==================
*/
static def_t *Def_DefTypeForCommand (){

	const char	*name;
	def_t		*def;
	int			i;

	name = Cmd_Argv(0);

	// Check if the current command matches any of our registered commands
	for (i = 0, def = def_defTypes; i < DEF_MAX_TYPES; i++, def++){
		if (!def->registered)
			continue;

		if (!Str_ICompare(def->listCommand, name) || !Str_ICompare(def->printCommand, name))
			return def;
	}

	return NULL;
}

/*
 ==================
 Def_ArgCompletion_DefName
 ==================
*/
static void Def_ArgCompletion_DefName (void (*callback)(const char *string)){

	def_t		*def;
	defData_t	*defData;
	int			i;

	// Identify the definition type
	def = Def_DefTypeForCommand();
	if (!def)
		return;		// Not supposed to happen!

	// Enumerate all the definitions of the given type
	for (i = 0; i < def->numDefs; i++){
		defData = def->defs[i];

		callback(defData->name);
	}
}

/*
 ==================
 Def_ListDefsByType_f
 ==================
*/
static void Def_ListDefsByType_f (){

	def_t		*def;
	defData_t	*defData;
	int			bytes = 0;
	int			i;

	// Identify the definition type
	def = Def_DefTypeForCommand();
	if (!def)
		return;		// Not supposed to happen!

	// List all the definitions of the given type
	Com_Printf("\n");
	Com_Printf("         -name-----------\n");

	for (i = 0; i < def->numDefs; i++){
		defData = def->defs[i];

		bytes += defData->textLength;

		Com_Printf("%5i: %s %s\n", i, (defData->referenced) ? "*" : " ", defData->name);
	}

	Com_Printf("-------------------------\n");
	Com_Printf("%i total %s definitions\n", def->numDefs, def->typeName);
	Com_Printf("%.2f MB of %s definition text\n", SIZE_MB_FLOAT(bytes), def->typeName);
	Com_Printf("\n");
}

/*
 ==================
 Def_PrintDefByType_f
 ==================
*/
static void Def_PrintDefByType_f (){

	def_t		*def;
	defData_t	*defData;
	const char	*name;
	uint		hashKey;

	// Identify the definition type
	def = Def_DefTypeForCommand();
	if (!def)
		return;		// Not supposed to happen!

	if (Cmd_Argc() != 2){
		Com_Printf("Usage: %s <name>\n", def->printCommand);
		return;
	}

	name = Cmd_Argv(1);

	// Find the definition of the given type
	hashKey = Str_HashKey(name, DEFS_HASH_SIZE, false);

	for (defData = def->hashTable[hashKey]; defData; defData = defData->nextHash){
		if (!Str_ICompare(defData->name, name))
			break;
	}

	if (!defData){
		Com_Printf("Couldn't find %s definition '%s'\n", def->typeName, name);
		return;
	}

	// Print it
	Com_Printf("\n");
	Com_Printf("Source: %s (line: %i)\n", defData->sourceFile, defData->sourceLine);
	Com_Printf("Referenced: %s\n", (defData->referenced) ? "yes" : "no");
	Com_Printf("\n");
	Com_Printf("%s%s\n", defData->name, defData->textString);
	Com_Printf("\n");
}

/*
 ==================
 Def_EnsureAlloced
 ==================
*/
static void Def_EnsureAlloced (def_t *def){

	defData_t	**defs;

	if (!def->defs){
		def->maxDefs = DEFS_GROW_SIZE;

		def->defs = (defData_t **)Mem_Alloc(def->maxDefs * sizeof(defData_t *), TAG_COMMON);

		return;
	}

	if (def->numDefs < def->maxDefs)
		return;

	def->maxDefs += DEFS_GROW_SIZE;

	defs = (defData_t **)Mem_Alloc(def->maxDefs * sizeof(defData_t *), TAG_COMMON);
	Mem_Copy(defs, def->defs, def->numDefs * sizeof(defData_t *));

	Mem_Free(def->defs);
	def->defs = defs;
}

/*
 ==================
 Def_LoadAndParse

 This is used during both the initial load, and any reloads
 ==================
*/
static void Def_LoadAndParse (def_t *def, const char *name){

	defData_t	*defData;
	script_t	*script;
	token_t		token;
	const char	*text;
	int			length;
	uint		hashKey;

	// Load the script file
	script = PS_LoadScriptFile(name);
	if (!script){
		Com_Printf("Couldn't load %s\n", name);
		return;
	}

	PS_SetScriptFlags(script, SF_NOWARNINGS | SF_NOERRORS | SF_ALLOWPATHNAMES);

	// Parse it
	while (1){
		// Parse the name
		if (!PS_ReadToken(script, &token))
			break;		// End of script

		// Parse the script
		text = script->text;

		PS_SkipBracedSection(script, 0);

		length = script->text - text;

		// See if there's an existing definition with this name
		hashKey = Str_HashKey(token.string, DEFS_HASH_SIZE, false);

		for (defData = def->hashTable[hashKey]; defData; defData = defData->nextHash){
			if (!Str_ICompare(defData->name, token.string))
				break;
		}

		// If it already exists, replace it
		if (defData){
			Mem_Free(defData->textString);

			defData->textString = (char *)Mem_Alloc(length + 1, TAG_COMMON);

			Str_Copy(defData->textString, text, length + 1);
			defData->textLength = length;
			Str_Copy(defData->sourceFile, script->name, sizeof(defData->sourceFile));
			defData->sourceLine = token.line;

			continue;
		}

		// Make sure the definition list is big enough
		Def_EnsureAlloced(def);

		// Add a new definition
		def->defs[def->numDefs++] = defData = (defData_t *)Mem_Alloc(sizeof(defData_t), TAG_COMMON);

		defData->textString = (char *)Mem_Alloc(length + 1, TAG_COMMON);

		// Fill it in
		Str_Copy(defData->name, token.string, sizeof(defData->name));
		defData->referenced = true;
		Str_Copy(defData->textString, text, length + 1);
		defData->textLength = length;
		Str_Copy(defData->sourceFile, script->name, sizeof(defData->sourceFile));
		defData->sourceLine = token.line;

		// Add to hash table
		defData->nextHash = def->hashTable[hashKey];
		def->hashTable[hashKey] = defData;
	}

	// Free the script file
	PS_FreeScript(script);
}


/*
 ==============================================================================

 PUBLIC INTERFACE

 ==============================================================================
*/


/*
 ==================
 Def_RegisterType
 ==================
*/
void Def_RegisterType (defType_t type, const char *typeName, const char *filePath, const char *fileExtension, const char *listCommand, const char *printCommand){

	def_t	*def;

	if (type < 0 || type >= DEF_MAX_TYPES)
		Com_Error(true, "Def_RegisterType: type out of range");

	def = &def_defTypes[type];

	if (def->registered)
		return;		// Already registered

	// Register the definition type
	def->registered = true;

	def->type = type;
	def->typeName = Mem_DupString(typeName, TAG_COMMON);

	Str_Copy(def->filePath, filePath, sizeof(def->filePath));
	Str_Copy(def->fileExtension, fileExtension, sizeof(def->fileExtension));

	Str_Copy(def->listCommand, listCommand, sizeof(def->listCommand));
	Str_Copy(def->printCommand, printCommand, sizeof(def->printCommand));

	// Add the associated commands
	Cmd_AddCommand(def->listCommand, Def_ListDefsByType_f, Str_VarArgs("Lists %s definitions", typeName), NULL);
	Cmd_AddCommand(def->printCommand, Def_PrintDefByType_f, Str_VarArgs("Prints a specified %s definition", typeName), Def_ArgCompletion_DefName);
}

/*
 ==================
 Def_LoadDefs
 ==================
*/
void Def_LoadDefs (defType_t type, bool print){

	def_t		*def;
	char		name[MAX_PATH_LENGTH];
	const char	**fileList;
	int			numFiles;
	int			i;

	if (type < 0 || type >= DEF_MAX_TYPES)
		Com_Error(true, "Def_LoadDefs: type out of range");

	def = &def_defTypes[type];

	if (!def->registered)
		return;

	// Scan for definition files
	fileList = FS_ListFiles(def->filePath, def->fileExtension, true, &numFiles);

	// Load and parse all the definitions
	for (i = 0; i < numFiles; i++){
		Str_SPrintf(name, sizeof(name), "%s/%s", def->filePath, fileList[i]);

		if (print)
			Com_Printf("...loading '%s'\n", name);

		Def_LoadAndParse(def, name);
	}

	FS_FreeFileList(fileList);
}

/*
 ==================
 Def_PurgeDefs
 ==================
*/
void Def_PurgeDefs (defType_t type){

	def_t		*def;
	defData_t	*defData;
	int			i;

	if (type < 0 || type >= DEF_MAX_TYPES)
		Com_Error(true, "Def_PurgeDefs: type out of range");

	def = &def_defTypes[type];

	if (!def->registered)
		return;

	// Purge all the definitions
	for (i = 0; i < def->numDefs; i++){
		defData = def->defs[i];

		if (defData->textString)
			Mem_Free(defData->textString);

		Mem_Free(defData);
	}

	if (def->defs)
		Mem_Free(def->defs);

	// Clear definition list
	def->numDefs = 0;
	def->maxDefs = 0;
	def->defs = NULL;

	Mem_Fill(def->hashTable, 0, sizeof(def->hashTable));
}

/*
 ==================
 Def_EnumDefs
 ==================
*/
void Def_EnumDefs (defType_t type, void (*callback)(const char *name)){

	def_t		*def;
	defData_t	*defData;
	int			i;

	if (type < 0 || type >= DEF_MAX_TYPES)
		Com_Error(true, "Def_EnumDefs: type out of range");

	def = &def_defTypes[type];

	if (!def->registered)
		return;

	// Enumerate all the definitions
	for (i = 0; i < def->numDefs; i++){
		defData = def->defs[i];

		callback(defData->name);
	}
}

/*
 ==================
 Def_GetNumDefs
 ==================
*/
int Def_GetNumDefs (defType_t type){

	def_t	*def;

	if (type < 0 || type >= DEF_MAX_TYPES)
		Com_Error(true, "Def_GetNumDefs: type out of range");

	def = &def_defTypes[type];

	if (!def->registered)
		return 0;

	return def->numDefs;
}

/*
 ==================
 Def_GetDefByIndex
 ==================
*/
defData_t *Def_GetDefByIndex (defType_t type, int index){

	def_t	*def;

	if (type < 0 || type >= DEF_MAX_TYPES)
		Com_Error(true, "Def_GetDefByIndex: type out of range");

	def = &def_defTypes[type];

	if (!def->registered)
		return NULL;

	if (index < 0 || index >= def->numDefs)
		return NULL;

	def->defs[index]->referenced = true;

	return def->defs[index];
}

/*
 ==================
 Def_FindDef
 ==================
*/
defData_t *Def_FindDef (defType_t type, const char *name){

	def_t		*def;
	defData_t	*defData;
	uint		hashKey;

	if (type < 0 || type >= DEF_MAX_TYPES)
		Com_Error(true, "Def_FindDef: type out of range");

	def = &def_defTypes[type];

	if (!def->registered)
		return NULL;

	hashKey = Str_HashKey(name, DEFS_HASH_SIZE, false);

	for (defData = def->hashTable[hashKey]; defData; defData = defData->nextHash){
		if (!Str_ICompare(defData->name, name)){
			defData->referenced = true;

			return defData;
		}
	}

	return NULL;
}


/*
 ==============================================================================

 CONSOLE COMMANDS

 ==============================================================================
*/


/*
 ==================
 Def_ListDefs_f
 ==================
*/
static void Def_ListDefs_f (){

	def_t		*def;
	defData_t	*defData;
	int			total = 0, bytes = 0;
	int			size;
	int			i, j;

	Com_Printf("\n");

	for (i = 0, def = def_defTypes; i < DEF_MAX_TYPES; i++, def++){
		if (!def->registered)
			continue;

		size = 0;

		for (j = 0; j < def->numDefs; j++){
			defData = def->defs[j];

			size += defData->textLength;
		}

		total += def->numDefs;
		bytes += size;

		Com_Printf("%4.2f MB in %5i %16s definitions\n", SIZE_MB_FLOAT(size), def->numDefs, def->typeName);
	}

	Com_Printf("---------------------------------------------\n");
	Com_Printf("%i total definitions\n", total);
	Com_Printf("%.2f MB of definition text\n", SIZE_MB_FLOAT(bytes));
	Com_Printf("\n");
}


/*
 ==============================================================================

 INITIALIZATION & SHUTDOWN

 ==============================================================================
*/


/*
 ==================
 Def_Init
 ==================
*/
void Def_Init (){

	// Add commands
	Cmd_AddCommand("listDefs", Def_ListDefs_f, "Lists all definitions", NULL);
}

/*
 ==================
 Def_Shutdown
 ==================
*/
void Def_Shutdown (){

	def_t		*def;
	defData_t	*defData;
	int			i, j;

	// Remove commands
	Cmd_RemoveCommand("listDefs");

	// Free all the definition types
	for (i = 0, def = def_defTypes; i < DEF_MAX_TYPES; i++, def++){
		if (!def->registered)
			continue;

		// Remove the associated commands
		Cmd_RemoveCommand(def->listCommand);
		Cmd_RemoveCommand(def->printCommand);

		// Free the allocated memory
		if (def->typeName)
			Mem_Free(def->typeName);

		for (j = 0; j < def->numDefs; j++){
			defData = def->defs[j];

			if (defData->textString)
				Mem_Free(defData->textString);

			Mem_Free(defData);
		}

		if (def->defs)
			Mem_Free(def->defs);
	}

	// Clear definition type list
	Mem_Fill(def_defTypes, 0, sizeof(def_defTypes));
}